/**************************************************************************************
*
*	Copyright (C) 2009 - 2012 Brent Andrew O. Chua - See LICENSE.txt for more info.
*		
*	Filename	-	Window.cpp
*
*	Comments	-	See Window.h
*
**************************************************************************************/
#include "../Include/Window.h"
#include "../Include/Engine.h"
#include "../Include/Application.h"
#include "../Include/WindowingSystem.h"
#include "../Include/EventWindowing.h"
#include "../Include/GraphicsResourceManager.h"
#include "../Include/SwapChain.h"
#include "../Include/RenderView.h"
#include "../Include/RenderEntryPresent.h"
#include "../Include/RenderEntryClearPipeline.h"
#include "../Include/RenderEntryRenderTarget.h"
#include "../Include/String.h"

LRESULT CALLBACK WindowProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
	LONG_PTR objPtr = GetWindowLongPtr( hWnd, 0 );

	if (objPtr == 0 )
	{
		return DefWindowProc( hWnd, msg, wParam, lParam );
	}
	else
	{
		Pulse::Window *pWindow = (Pulse::Window*)objPtr;

		//if ( pWindow->GetHandle() == hWnd )
			return pWindow->WindowProc( hWnd, msg, wParam, lParam );
		//else
			//return DefWindowProc( hWnd, msg, wParam, lParam );

	}
}

namespace Pulse
{
	Window::Window( const CHAR *pWindowName )
	{
		m_name = pWindowName;
		m_bMainWindow = FALSE;

		m_posX = 0;
		m_posY = 0;
		m_width = 683;
		m_height = 384;

		m_handle = PSX_NULL;

		m_bMinimized	= FALSE;
		m_bWindowed		= TRUE;
		m_bClosing		= FALSE;
		m_bSafeToClose	= FALSE;

		m_pSwapChain = PSX_NULL;
		m_pRenderView = PSX_NULL;
		m_pREClearPipeline = PSX_NULL;
		m_pRERenderTarget = PSX_NULL;
		m_pREPresent = PSX_NULL;

		m_handle = PSX_NULL;
		pMsg = PSX_NULL;
	}

	Window::~Window( void )
	{
		Cleanup();
	}

	EErrorCode::Type Window::Create( const CHAR *pWindowName, Window **ppOut, BOOL bMainWindow )
	{
		if ( ppOut == PSX_NULL )
		{
			PSX_PushError( "Window can't have an empty name." );
			return EErrorCode::INVALID_PARAMETER;
		}

		// Make sure we don't create a window with no name
		if ( pWindowName == PSX_NULL || PSX_StrLen( pWindowName ) == 0 )
		{
			*ppOut = PSX_NULL;
			PSX_PushError( "Window can't have an empty name." );
			return EErrorCode::INVALID_PARAMETER;
		}

		Window *pNewWindow = new Window( pWindowName );

		if ( pNewWindow  == PSX_NULL )
		{
			PSX_PushError( "Out of memory." );
			return EErrorCode::MEMORY;
		}

		if ( bMainWindow )
		{
			pNewWindow->m_bMainWindow = TRUE;
		}

		pNewWindow->Initialize();

		*ppOut = pNewWindow;

		return EErrorCode::OKAY;
	}
	
	void Window::Delete( Window *pWindow )
	{
		//pWindow->Cleanup(); // implicitly called
		delete pWindow;
	}

	void Window::Update( const FLOAT dt )
	{
		MSG *pMessage = (MSG*)pMsg;

		if ( pMessage->message != WM_QUIT )
		{
			if ( PeekMessage( pMessage, (HWND)m_handle, 0, 0, PM_REMOVE ) )
			{
				// This code handles message events
				TranslateMessage( pMessage );
				DispatchMessage( pMessage );
			}
			else
			{

				//if ( m_bQuitting )
				//	continue;

				// Process app code if we don't have any windows messages.
				//System::Update();

				if ( !m_bMinimized && !m_bClosing )
				{
					if ( /*!m_paused*/ TRUE ) 
					{
						m_eventSystem.ProcessEvents();
						// Perform WindowView updates here...
						for ( SIZE_T i = 0; i < m_windowViews.GetSize(); ++i )
						{
							m_windowViews[i]->Update( dt );

						}
					}

					//OnRender();
				}
				else if ( m_bClosing && !m_bSafeToClose )
				{
					Cleanup();
					DestroyWindow( (HWND)m_handle );
					m_bSafeToClose = TRUE;
				}

				//// Handle FPS debug info
				//if ( m_bWindowed && m_bShowFPS ) 
				//	HandleFPS();
			}
		}
	}

	void Window::Render( void )
	{
		if ( m_bMinimized )
		{
			return;
		}

		// Render each WindowViews to present to their render textures
		for ( SIZE_T i = 0; i < m_windowViews.GetSize(); ++i )
		{
			m_windowViews[i]->Render();
		}

		// Now we need to present each WindowView's render texture to the swapchain
		for ( SIZE_T i = 0; i < m_windowViews.GetSize(); ++i )
		{
			m_windowViews[i]->RenderToWindow( m_pRenderView );
		}

		// Add last Rentry entry, swapchain
		m_pRenderView->AddRenderEntry( m_pRERenderTarget );
		m_pRenderView->AddRenderEntry( m_pREPresent );
		m_pRenderView->AddRenderEntry( m_pREClearPipeline );

		// Present all the content of this window to the swapchain
		Engine::GetInstance()->GetRenderer()->GetRenderPath()->ExecuteRenderView( m_pRenderView );
	}

	EErrorCode::Type Window::Initialize( void )
	{
		pMsg = new MSG;
		PSX_ZeroMem( pMsg, sizeof(MSG) );

		WNDCLASSEX wndClass;
		String className = PSX_String("Pulse-Tec: ") + m_name;
		PSX_ZeroMem( &wndClass, sizeof( WNDCLASSEX ) );

		// This is a definition for the window we're creating.
		wndClass.cbSize			= sizeof( WNDCLASSEX );
		wndClass.style			= CS_BYTEALIGNCLIENT | CS_HREDRAW | CS_VREDRAW /*| CS_DBLCLKS*/;
		wndClass.lpfnWndProc	= &::WindowProc;
		wndClass.cbClsExtra		= 0;
		wndClass.cbWndExtra		= sizeof( this );
		//wndClass.hInstance		= m_hInstance;
		wndClass.hInstance		= PSX_NULL;
		wndClass.hIcon			= LoadIcon( NULL, IDI_APPLICATION );
		wndClass.hCursor		= LoadCursor( NULL, IDC_ARROW );
		wndClass.hbrBackground	= (HBRUSH)GetStockObject( BLACK_BRUSH );
		wndClass.lpszClassName	= className.GetBuffer();
		wndClass.lpszMenuName	= NULL;
		wndClass.hIconSm		= NULL;

		if ( ::RegisterClassEx( &wndClass ) == 0 )
		{
			PSX_PushError( "Failed to register Win32 window class." );
			return EErrorCode::_ERROR;
		}

		// Define style
		#define WS_NORMAL_WINDOW (WS_OVERLAPPEDWINDOW & ~(/*WS_THICKFRAME|*/WS_MAXIMIZEBOX))
		#define WS_FULLSCREEN_WINDOW (WS_POPUP)
		DWORD style = WS_NORMAL_WINDOW;

		// Define window size
		RECT rect;

		rect.left = m_posX;
		rect.top = m_posY;
		rect.right = m_posX + (LONG)m_width;
		rect.bottom = m_posY + (LONG)m_height;

		AdjustWindowRect( &rect, style, FALSE );

		HWND hWnd = CreateWindowEx(
			NULL,							// extended style
			wndClass.lpszClassName, 		// class name
			m_name.GetBuffer(),				// instance title
			style,							// window style
			rect.left, rect.top,			// initial x, y
			rect.right - rect.left,			// initial width
			rect.bottom - rect.top,			// initial height
			NULL,							// handle to parent 
			NULL,							// handle to menu
			NULL,							// instance of this application
			NULL );							// extra creation parms);

		if ( hWnd == 0 )
		{
			PSX_PushError( "Failed to create Win32 Window." );
			return EErrorCode::_ERROR;
		}

		// I don't know why i cant directly use m_handle
		m_handle = hWnd;

		// Store this instance in the windows extra storage
		SetWindowLongPtr( hWnd, 0, (LONG_PTR)this );
		ShowWindow( hWnd, SW_SHOWNORMAL );
		UpdateWindow( hWnd );

		if ( m_bWindowed )
		{
			String tittle;
			tittle.Format( PSX_String("%s: %d"), m_name.GetBuffer(), m_handle ); 
			SetWindowText( (HWND)m_handle, tittle.GetBuffer() );
		}

		FixWindowSize();

		// Eveything's good, now create swapchain and renderview, and render entry
		{
			SwapChainDesc swapChainDesc;

			swapChainDesc.Set( m_width, m_height, Rational( 60.0f, 1.0f ),
				EGraphicsFormat::R8G8B8A8_UNORM, SampleDesc(), m_bWindowed, m_handle );
			//swapChainDesc.SetAsyncLoad( TRUE );

			m_pSwapChain = GraphicsResourceManager::GetInstance()->CreateSwapChain( &swapChainDesc );
		
			m_pRenderView = new RenderView( PSX_NULL );

			if ( m_pRenderView == PSX_NULL )
			{
				PSX_PushError( "Out of memory." );
				return EErrorCode::MEMORY;
			}

			m_pRERenderTarget = new RenderEntryRenderTarget;
			m_pREPresent = new RenderEntryPresent;
			m_pREClearPipeline = new RenderEntryClearPipeline( TRUE, TRUE, TRUE, TRUE, TRUE, FALSE );

			if ( m_pRERenderTarget == PSX_NULL || m_pREPresent == PSX_NULL || m_pREClearPipeline ==PSX_NULL )
			{
				PSX_PushError( "Out of memory." );
				return EErrorCode::MEMORY;
			}
			
			m_pRERenderTarget->SetStage( ERenderViewStage::WINDOW_VIEW );
			m_pRERenderTarget->SetStageStep( ERenderViewStageStep::PRE_PROCESS );
			m_pRERenderTarget->SetClearBuffers( TRUE );
			m_pRERenderTarget->SetRenderTarget( 0, m_pSwapChain->GetBackBufferUnsafe() );

			m_pREPresent->SetStage( ERenderViewStage::PRESENT );
			m_pREPresent->SetStageStep( ERenderViewStageStep::PROCESS );
			//m_pREPresent->SetStage( ERenderViewStage::PRESENT );
			m_pREPresent->SetSwapChain( m_pSwapChain );

			m_pREClearPipeline->SetStage( ERenderViewStage::WINDOW_VIEW );
			m_pREClearPipeline->SetStageStep( ERenderViewStageStep::POST_PROCESS );
		}

		return EErrorCode::OKAY;
	}

	void Window::Cleanup( void )
	{
		RemoveAllWindowViews();

		PSX_SafeRelease( m_pSwapChain );
		PSX_SafeDelete( m_pRenderView );
		PSX_SafeDelete( m_pREClearPipeline );
		PSX_SafeDelete( m_pRERenderTarget );
		PSX_SafeDelete( m_pREPresent );

		PSX_SafeDelete( pMsg );
	}

	void Window::FixWindowSize( void )
	{
		// Windowed
		RECT r = { m_posX, m_posY, (LONG)(m_posX + m_width), (LONG)(m_posY + m_height) };
		//SetWindowLong( (HWND)m_handle, GWL_STYLE, WS_NORMAL_WINDOW );
		//::SetWindowPos( m_hWnd, HWND_NOTOPMOST, m_appPosX, m_appPosY, GetWidth(), GetHeight(), SWP_NOACTIVATE | SWP_SHOWWINDOW );

		// Adjust window rect
		AdjustWindowRectEx( &r, (DWORD)GetWindowLongPtr( (HWND)m_handle, GWL_STYLE), FALSE, 
			(DWORD)GetWindowLongPtr( (HWND)m_handle, GWL_EXSTYLE ) );

		INT deltaX( m_posX - r.left ), deltaY( m_posY - r.top );
		r.left += deltaX; r.right += deltaX;
		r.top += deltaY; r.bottom += deltaY;

		::SetWindowPos( (HWND)m_handle, 0, r.left, r.top, r.right - r.left, r.bottom - r.top, SWP_NOZORDER );
	}

	void Window::RemoveAllWindowViews( void )
	{
		WindowingSystem *pWindowSys = Application::GetInstance()->GetWindowingSystem();

		while ( m_windowViews.GetSize() )
		{
			WindowView *pView = m_windowViews[0];
			
			EErrorCode::Type retCode = pWindowSys->RemoveWindowView( pView->GetName() );
		
			if ( retCode != EErrorCode::OKAY )
			{
				PSX_PushError( "Removing a WindowView should always succeed. Something is wrong with pView." );
			}
		}
	}

	EErrorCode::Type Window::FromWindowSysRemoveView( const WindowView *pView )
	{
		for ( SIZE_T i = 0; i < m_windowViews.GetSize(); ++i )
		{
			if ( m_windowViews[i] == pView )
			{
				m_windowViews.Remove( i );
				return EErrorCode::OKAY;
			}
		}

		return EErrorCode::NOT_FOUND;
	}

	void Window::UpdateWindowViewSizes()
	{
		for ( SIZE_T i = 0; i < m_windowViews.GetSize(); ++i )
		{
			WindowView *pView = m_windowViews[i];
		
			pView->ResetOrientation();
		}
	}

	LONG_PTR Window::OnGainFocus( UINT msg, WPARAM wParam, LPARAM lParam )
	{
		PSX_PushLog3( "OnGainFocus: %d", m_handle );

		//Engine::GetInstance()->GetEventSystem()->TriggerEvent( &EventWindowGainFocus( this, (INT)m_handle ) );
		
		//Application::GetInstance()->GetWindowingSystem()->

		EventPtr event = new EventWindowGainFocus( this, (INT)m_handle );
		Engine::GetInstance()->GetEventSystem()->PostEvent( event );


		// Register events
		{
			Application *pApp = Application::GetInstance();
			EventSystem *pEventSystem = pApp->GetWindowingSystem()->GetEventSystem();

			pEventSystem->AddListener( EEvent::INPUT_KEY_UP, this, &Window::OnKeyUp );
			pEventSystem->AddListener( EEvent::INPUT_KEY_DOWN, this, &Window::OnKeyDown );
			pEventSystem->AddListener( EEvent::INPUT_MOUSE_MOVE, this, &Window::OnMouseMove );
			pEventSystem->AddListener( EEvent::INPUT_MOUSE_BUTTONUP, this, &Window::OnMouseButtonUp );
			pEventSystem->AddListener( EEvent::INPUT_MOUSE_BUTTONDOWN, this, &Window::OnMouseButtonDown );
		}

		return 0;
	}

	LONG_PTR Window::OnLoseFocus( UINT msg, WPARAM wParam, LPARAM lParam )
	{
		PSX_PushLog3( "OnLoseFocus: %d", m_handle );
		
		//Engine::GetInstance()->GetEventSystem()->TriggerEvent( &EventWindowLoseFocus( this, (INT)m_handle ) );
		
		EventPtr event = new EventWindowLoseFocus( this, (INT)m_handle );
		Engine::GetInstance()->GetEventSystem()->PostEvent( event );

		// Unregister events
		{
			Application *pApp = Application::GetInstance();
			EventSystem *pEventSystem = pApp->GetWindowingSystem()->GetEventSystem();

			pEventSystem->RemoveListener( EEvent::INPUT_KEY_UP, this, &Window::OnKeyUp );
			pEventSystem->RemoveListener( EEvent::INPUT_KEY_DOWN, this, &Window::OnKeyDown );
			pEventSystem->RemoveListener( EEvent::INPUT_MOUSE_MOVE, this, &Window::OnMouseMove );
			pEventSystem->RemoveListener( EEvent::INPUT_MOUSE_BUTTONUP, this, &Window::OnMouseButtonUp );
			pEventSystem->RemoveListener( EEvent::INPUT_MOUSE_BUTTONDOWN, this, &Window::OnMouseButtonDown );
		}

		return 0;
	}

	LONG_PTR Window::OnSizeMoveEnter( UINT msg, WPARAM wParam, LPARAM lParam )
	{
		return DefWindowProc( (HWND)m_handle, msg, wParam, lParam );
	}

	LONG_PTR Window::OnSizeMoveExit( UINT msg, WPARAM wParam, LPARAM lParam )
	{
		//GetWindowPos( (HWND)m_handle, 
		RECT clientRect, windowRect;

		GetClientRect( (HWND)m_handle, &clientRect );
		GetWindowRect( (HWND)m_handle, &windowRect );

		if ( clientRect.right != m_width || clientRect.bottom != m_height )
		{

			m_posX = windowRect.left;
			m_posY = windowRect.top;

			// Window has been resized
			Resize( clientRect.right, clientRect.bottom );
		}

		if ( windowRect.left != m_posX || windowRect.top != m_posY )
		{
			// Window has been moved
		}

		return 0;
	}

	LONG_PTR Window::OnMove( UINT msg, WPARAM wParam, LPARAM lParam )
	{
		//PSX_PushLog3( "Moving: %d", m_handle );
		return DefWindowProc( (HWND)m_handle, msg, wParam, lParam );
	}

	LONG_PTR Window::OnClose( UINT msg, WPARAM wParam, LPARAM lParam )
	{
		// TODO: Prompt the user if we should close or not

		PSX_PushLog3( "OnClose: %d", m_handle );
		
		// TODO: Shutdown should be performed here before destroy window
		if ( m_bClosing == FALSE )
		{
			m_bClosing = TRUE; 
			//m_bQuitting = FALSE;
		}

		//return DefWindowProc( (HWND)m_handle, msg, wParam, lParam );
		return 0;
	}

	LONG_PTR Window::OnQuit( UINT msg, WPARAM wParam, LPARAM lParam )
	{
		PSX_PushLog3( "OnQuit: %d", m_handle );
		return DefWindowProc( (HWND)m_handle, msg, wParam, lParam );
	}

	LONG_PTR Window::OnDestroy( UINT msg, WPARAM wParam, LPARAM lParam )
	{
		PSX_PushLog3( "OnDestroy: %d", m_handle );
		// If this is the main window, terminate entire program. Otherwise don't.
		if ( m_bMainWindow )
		{

			// NOTE: Anything we wanna pass here?
			PostQuitMessage( 0 );
		}

		return 0;
	}

	void Window::Resize( SIZE_T width, SIZE_T height )
	{
		m_width = width;
		m_height = height;

		FixWindowSize();

		UpdateWindowViewSizes();

		// TODO: Release all references then resize swapchain
		// TODO: Releasing all references requires sending an event
		EErrorCode::Type retCode = m_pSwapChain->Resize( width, height );
		m_pRERenderTarget->SetRenderTarget( 0, m_pSwapChain->GetBackBufferUnsafe() );
	
		if ( retCode != EErrorCode::OKAY )
		{
			PSX_PushError( "Failed to resize SwapChain buffer." );
		}
	}

	LONG_PTR Window::WindowProc( void *hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
	{
		// Messages will be delegated to other functions "On...()" as if it fired an event.
		switch( msg )
		{
		case WM_CREATE:
			return TRUE;
		case WM_ACTIVATEAPP:
			// TODO: May need to add functionality for this
			//if ( wParam )
			//	return OnGainFocus( msg, wParam, lParam );
			//else
			//	return OnLoseFocus( msg, wParam, lParam );
			break;
		case WM_ACTIVATE:
			if ( wParam ) // 1 - Activated other than mouse click. 2 - activated with mouse click
				return OnGainFocus( msg, wParam, lParam );
			else
				return OnLoseFocus( msg, wParam, lParam );
			break;
		//case WM_WINDOWPOSCHANGED:
		//	PSX_PushLog1( "Done moving" );
		case WM_SIZE:
			if( wParam == SIZE_MINIMIZED )
			{
				// gpApp->OnWMLoseFocus();
				m_bMinimized = TRUE;
			}
			else if( wParam == SIZE_RESTORED )
			{
				if( m_bMinimized )  // Restoring from minimized state?
				{
					//gpApp->OnWMGainFocus();
					m_bMinimized = FALSE;
				}
				else if ( m_bWindowed )
				{
					//gpApp->m_bMinimized = FALSE;
					//::ShowWindow( gpApp->m_hWnd, SW_MINIMIZE );
					//gppp->m_bMinimized = TRUE;
					// gpApp->ShowWindow( SW_MINIMIZE );
					//Resize( LOWORD( lParam ), HIWORD( lParam ) );
				}
			}
			return 0;
		case WM_ENTERSIZEMOVE:
			return OnSizeMoveEnter( msg, wParam, lParam );
		case WM_EXITSIZEMOVE:
			return OnSizeMoveExit( msg, wParam, lParam );
		case WM_MOVE:
			return OnMove( msg, wParam, lParam );
		case WM_SYSCOMMAND:
			// Eat Alt-F4.
			if ( wParam == SC_CLOSE )
				//return 0; // Allow SC_CLOSE for now...
					break;
			else
				break;
		case WM_CLOSE:
			return OnClose( msg, wParam, lParam );
		case WM_QUIT:	// NOTE: Not used... The message loop is terminated before this message is sent
			return OnQuit( msg, wParam, lParam );
			//case WM_KILLFOCUS:
		case WM_DESTROY:
			return OnDestroy( msg, wParam, lParam );
		}

		// Pass it to default procedure if unhandled.
		return DefWindowProc( (HWND)hWnd, msg, wParam, lParam );
	}

	EErrorCode::Type Window::InsertWindowView( WindowView *pView )
	{
		// Before anything else, do some error checks

		if ( pView == PSX_NULL )
		{
			PSX_PushError( "Invalid arguments." );
			return EErrorCode::INVALID_PARAMETER;;
		}

		if ( FindWindowView( pView->GetName() ) != PSX_NULL )
		{
			PSX_PushError( "WindowView already in this window!" );
			return EErrorCode::ALREADY_EXISTS;
		}

		// Safe to add
		pView->SetOwner( this );
		m_windowViews.PushBack( pView );


		// NOTE: Handle insert behavior. There are many cases and insert behaviors to consider.
		/*
			The very basic and common one is to have no views at first. So the 
			view to be inserted takes the entire screen of the window
		*/
		if ( GetNumWindowViews() == 1 ) // The view is the only one
		{
			// Maximize view space
			EErrorCode::Type retCode = pView->ResetOrientation( 0, 0, 1.0f, 1.0f );

			if ( retCode != EErrorCode::OKAY )
			{
				return retCode;
			}
		}
		else
		{
			PSX_PushError( "Not yet implemented." );
		}

		return EErrorCode::OKAY;
	}

	WindowView * Window::FindWindowView( const CHAR *pName )
	{
		String nameToCompare = pName;

		for ( SIZE_T i = 0; i < m_windowViews.GetSize(); ++i )
		{
			if ( nameToCompare == m_windowViews[i]->GetName() )
			{
				return m_windowViews[i];
			}
		}

		return PSX_NULL;
	}

	void Window::OnKeyUp( IEvent *pEvent )
	{
		//EventKeyUp *pDerivedEvent = (EventKeyUp*)pEvent;
		m_eventSystem.PostEventUnmanaged( pEvent );
	}

	void Window::OnKeyDown( IEvent *pEvent )
	{
		//EventKeyDown *pDerivedEvent = (EventKeyDown*)pEvent;
		m_eventSystem.PostEventUnmanaged( pEvent );

	}

	void Window::OnMouseMove( IEvent *pEvent )
	{
		//EventMouseMove *pDerivedEvent = (EventMouseMove*)pEvent;
		m_eventSystem.PostEventUnmanaged( pEvent );

	}

	void Window::OnMouseButtonUp( IEvent *pEvent )
	{
		//EventMouseButtonUp *pDerivedEvent = (EventMouseButtonUp*)pEvent;
		m_eventSystem.PostEventUnmanaged( pEvent );

	}

	void Window::OnMouseButtonDown( IEvent *pEvent )
	{
		//EventMouseButtonDown *pDerivedEvent = (EventMouseButtonDown*)pEvent;
		m_eventSystem.PostEventUnmanaged( pEvent );

	}
}